package org.dromara.web.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.XcxLoginBody;
import org.dromara.common.core.domain.model.XcxLoginUser;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.log.event.LogininforEvent;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysConfigService;
import org.dromara.system.service.ISysUserService;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.springframework.stereotype.Service;

/**
 * 小程序认证策略
 *
 * @author Michelle.Chung
 */
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {

    private final SysUserMapper userMapper;
    private final ISysUserService userService;

    /**
     * 小程序执行登录操作
     *
     * @param body   登录对象
     * @param client 授权管理视图对象
     * @return 登录验证信息
     */
    @Override
    public LoginVo login(String body, SysClientVo client) {
        log.info("【小程序登录认证】执行用户登录操作, body：{}，client：{}", body, JsonUtils.toJsonString(client));

        // 解析用户登录接口请求参数信息
        XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);

        // 校验请求参数
        ValidatorUtils.validate(loginBody);

        // xcxCode 为 小程序调用 wx.login 授权后获取
        String xcxCode = loginBody.getXcxCode();
        // 多个小程序识别使用
        String appid = loginBody.getAppid();

        // 校验 appid + appSrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
        AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
            .clientId(appid).clientSecret("自行填写密钥 可根据不同appid填入不同密钥")
            .ignoreCheckRedirectUri(true).ignoreCheckState(true).build());

        // 构建小程序登录认证回调对象
        AuthCallback authCallback = new AuthCallback();
        authCallback.setCode(xcxCode);

        // 执行登录认证操作
        AuthResponse<AuthUser> resp = authRequest.login(authCallback);
        log.info("【小程序登录认证】执行用户登录操作, resp：{}", JsonUtils.toJsonString(resp));

        String openid;// 小程序openid
        String unionId;// 小程序unionId
        if (resp.ok()) {
            // 获取登录凭证Token对象
            AuthToken token = resp.getData().getToken();
            log.info("【小程序登录认证】执行用户登录操作成功, token：{}", JsonUtils.toJsonString(token));
            // 获取 openid
            openid = token.getOpenId();
            log.info("【小程序登录认证】执行用户登录操作成功, openid：{}", openid);
            // 微信小程序只有关联到微信开放平台下之后才能获取到 unionId，因此unionId不一定能返回。
            unionId = token.getUnionId();
            log.info("【小程序登录认证】执行用户登录操作成功, unionId：{}", unionId);
        } else {
            log.error("【小程序登录认证】执行用户登录操作失败, 错误信息：{}", resp.getMsg());
            throw new ServiceException(resp.getMsg());
        }

        // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
        SysUserVo user = loadUserByOpenid(openid, loginBody);

        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
        XcxLoginUser loginUser = new XcxLoginUser();
        loginUser.setTenantId(user.getTenantId());
        loginUser.setUserId(user.getUserId());
        loginUser.setUsername(user.getUserName());
        loginUser.setNickname(user.getNickName());
        loginUser.setUserType(user.getUserType());
        loginUser.setClientKey(client.getClientKey());
        loginUser.setDeviceType(client.getDeviceType());
        loginUser.setOpenid(openid);

        // 构建Sa登录参数
        SaLoginParameter model = new SaLoginParameter();
        // 设备类型
        model.setDeviceType(client.getDeviceType());
        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
        // 例如: 后台用户30分钟过期 app用户1天过期
        model.setTimeout(client.getTimeout());
        // token活跃超时时间
        model.setActiveTimeout(client.getActiveTimeout());
        // 设置额外扩展参数
        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
        log.info("【小程序登录认证】执行用户登录操作, model：{}", JsonUtils.toJsonString(model));

        // 生成token
        LoginHelper.login(loginUser, model);

        // 构建登录验证信息
        LoginVo loginVo = new LoginVo();
        loginVo.setAccessToken(StpUtil.getTokenValue());
        loginVo.setExpireIn(StpUtil.getTokenTimeout());
        loginVo.setClientId(client.getClientId());
        loginVo.setOpenid(openid);
        log.info("【小程序登录认证】执行用户登录操作成功, loginVo：{}", JsonUtils.toJsonString(loginVo));
        return loginVo;
    }

    /**
     * 根据openid查询用户信息
     *
     * @param openId    openId
     * @param loginBody 登录对象
     * @return 用户信息
     */
    private SysUserVo loadUserByOpenid(String openId, XcxLoginBody loginBody) {
        // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
        SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getOpenId, openId));
        if (ObjectUtil.isNull(user)) {
            log.info("登录用户：{} 不存在.", openId);
            // 用户不存在，则执行用户注册操作
            return registerUser(loginBody, openId);
        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
            // 用户已被停用 业务逻辑自行实现
            log.error("登录用户：{} 已被停用.", openId);
            throw new UserException("user.blocked", openId);
        }
        return user;
    }

    /**
     * 执行小程序用户注册操作
     *
     * @param loginBody 登录对象
     * @param openId    openId
     * @return 用户信息
     */
    private SysUserVo registerUser(XcxLoginBody loginBody, String openId) {
        // 用户管理-账号初始密码
        String initPassword = SpringUtils.getBean(ISysConfigService.class).selectConfigByKey("sys.user.initPassword");
        // 校验用户类型是否存在
        String userType = UserType.getUserType("wechat_xcx").getUserType();
        String userName = loginBody.getUserName();
        String nickName = loginBody.getNickName();
        String tenantId = loginBody.getTenantId();

        // 构建用户实体类
        SysUserBo sysUser = new SysUserBo();
        sysUser.setUserName(userName);
        sysUser.setNickName(nickName);
        sysUser.setPassword(BCrypt.hashpw(initPassword));
        sysUser.setUserType(userType);
        sysUser.setOpenId(openId);

        // 根据租户ID获取注册用户是否已存在账户信息
        boolean exist = TenantHelper.dynamic(tenantId, () -> userMapper.exists(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, sysUser.getUserName())));
        if (exist) {
            log.error("租户ID{} 用户注册失败，用户名：{} 已存在！", tenantId, userName);
            throw new UserException("user.register.save.error", userName);
        }

        // 执行注册用户信息
        boolean regFlag = userService.registerUser(sysUser, tenantId);
        if (!regFlag) {
            log.error("租户ID{} 用户注册失败，用户名：{} ", tenantId, userName);
            throw new UserException("user.register.error");
        }

        // 记录用户登录信息
        recordLogininfor(tenantId, userName, Constants.REGISTER, MessageUtils.message("user.register.success"));
        return null;
    }

    /**
     * 记录登录信息
     *
     * @param tenantId 租户ID
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     */
    private void recordLogininfor(String tenantId, String username, String status, String message) {
        LogininforEvent logininforEvent = new LogininforEvent();
        logininforEvent.setTenantId(tenantId);
        logininforEvent.setUsername(username);
        logininforEvent.setStatus(status);
        logininforEvent.setMessage(message);
        logininforEvent.setRequest(ServletUtils.getRequest());
        SpringUtils.context().publishEvent(logininforEvent);
    }

}
